﻿// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
//
// MainPage.xaml.cpp
// Implementation of the MainPage class
//

#include "pch.h"
#include <queue>
#include <random>
#include "BingLocationService.h"
#include "ExceptionPolicyFactory.h"
#include "GeonamesLocationService.h"
#include "LocationData.h"
#include "LocationsPage.xaml.h"
#include "LonLatController.h"
#include "MainPage.xaml.h"
#include "MessageDialog.h"
#include "UserSettings.h"
#include "WebException.h"
#include "WebServiceRegistry.h"

using namespace Weathr;

using namespace concurrency;
using namespace Platform;
using namespace Platform::Collections;
using namespace std;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Storage;
using namespace Windows::Storage::Streams;
using namespace Windows::UI::Core;
using namespace Windows::UI::StartScreen;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::UI::Xaml::Controls::Primitives;
using namespace Windows::UI::Xaml::Data;
using namespace Windows::UI::Xaml::Input;
using namespace Windows::UI::Xaml::Interop;
using namespace Windows::UI::Xaml::Media;
using namespace Windows::UI::Xaml::Media::Animation;
using namespace Windows::UI::Xaml::Media::Imaging;
using namespace Windows::UI::Xaml::Navigation;

const float TileUpdateIntervalInitial = 15.0; // First tile update should happen fairly early.
const float TimeUpdateIntervalFuture = 180.0; // Future tile updates should happen at later intervals.
float TileUpdateInterval = TileUpdateIntervalInitial;
String^ SecondaryTileId = "Weathr.SecondaryTile";

// The Basic Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234237

//
// Helpers
//

Rect GetElementRect(FrameworkElement^ element)
{
    Media::GeneralTransform^ buttonTransform = element->TransformToVisual(nullptr);
    Point point = buttonTransform->TransformPoint(Point(0, 0));
    return Rect(point, Size(static_cast<float>(element->ActualWidth), static_cast<float>(element->ActualHeight)));
}

// http://social.msdn.microsoft.com/Forums/windowsapps/en-US/263e267a-7d6c-4478-9679-a2ac1b0e83cc/how-do-i-access-appbar-buttons-programmatically
static AppBarButton^ GetButtonFromCommands(IObservableVector<ICommandBarElement^>^ commands, String^ name)
{
    for (auto iter = begin(commands); iter != end(commands); ++iter)
    {
        ICommandBarElement^ x = *iter;
        auto button = dynamic_cast<AppBarButton^>(x);
        if (button != nullptr)
        {
            if (button->Name == name)
            {
                return button;
            }
        }
    }
    return nullptr;
}

static AppBarButton^ GetButtonFromBar(CommandBar^ bar, String^ name)
{
    auto button = GetButtonFromCommands(bar->PrimaryCommands, name);
    if (button != nullptr)
    {
        return button;
    }
    button = GetButtonFromCommands(bar->SecondaryCommands, name);
    return button;
}

void PopupPlacementHelper(Popup^ popup, Button^ appBarButton, float32 height)
{
    height = min(height, static_cast<float>(popup->MaxHeight));
    popup->Height = height;

    // Place the popup directly above the command button.
    auto rect = GetElementRect(appBarButton);
    popup->SetValue(Canvas::LeftProperty, static_cast<double>(rect.Left));
    popup->SetValue(Canvas::TopProperty, static_cast<double>(rect.Top - height));
}

//
// MainPage
//

MainPage::MainPage()
    : m_exceptionPolicy(ExceptionPolicyFactory::GetCurrentPolicy())
    , m_forecastManager(ForecastManager::Current)
    , m_locationService(new Web::GeonamesLocationService(L"Content\\cities15000.txt", ExceptionPolicyFactory::GetCurrentPolicy()))
    , m_lastPointValid(false)
    , m_pinLocationResultBuffer([this](shared_ptr<Web::Location> result) { ProcessPinLocation(result); })
    , m_pinLocationErrorBuffer([this](Web::WebException error) { ProcessPinError(error); })
    , m_tileUpdateTimer(ref new BasicTimer())
    , m_updatingLiveTile(false)
    , m_updatingSecondaryTile(false)
{
    m_pin = ref new Pin(m_locationService);

    // Ensure page is cached (and not recreated) during navigation.
    NavigationCacheMode = Navigation::NavigationCacheMode::Required;

    InitializeComponent();
    SetValue(_defaultViewModelProperty, ref new PropertySet());
    auto navigationHelper = ref new Common::NavigationHelper(this);
    SetValue(_navigationHelperProperty, navigationHelper);
    navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &MainPage::LoadState);
    navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &MainPage::SaveState);

    // Add initial values to view model.
    DefaultViewModel->Insert("ForecastManager", m_forecastManager);
    DefaultViewModel->Insert("DataProviders", ref new Vector<String^>());
    DefaultViewModel->Insert("ForecastTile", nullptr);
    DefaultViewModel->Insert("IsSecondaryTileUpdate", false);
    DefaultViewModel->Insert("IsAsyncOperationActive", false);
    DefaultViewModel->Insert("Pin", m_pin);

    // Register to be notified when a web service is registered.
    WebServiceRegistry::WebServiceRegistered += ref new WebServiceRegisteredHandler(this, &MainPage::OnWebServiceRegistered);
    // Catch up on any services that were already registered.
    for (auto serviceName : WebServiceRegistry::GetRegisteredServices())
    {
        OnWebServiceRegistered(serviceName);
    }

    m_eventToken.Value = 0;

    // Start the locator agent that used by the pin feature.
    auto& agent = m_pin->GetLocatorAgent();
    agent.GetOutputBuffer()->link_target(&m_pinLocationResultBuffer);
    agent.GetErrorBuffer()->link_target(&m_pinLocationErrorBuffer);
    agent.start();

    // Register for pointer events used by the pin feature.
    CoreWindow::GetForCurrentThread()->PointerPressed += ref new TypedEventHandler<CoreWindow ^, PointerEventArgs ^>(this, &MainPage::OnPointerPressed);
    CoreWindow::GetForCurrentThread()->PointerMoved += ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(this, &MainPage::OnPointerMoved);
    CoreWindow::GetForCurrentThread()->PointerReleased += ref new TypedEventHandler<CoreWindow ^, PointerEventArgs^>(this, &MainPage::OnPointerReleased);

    // Register for weather data changed events.
    Forecast::WeatherDataChanged += ref new WeatherDataChangedHandler(this, &MainPage::OnWeatherDataChanged);
}

DependencyProperty^ MainPage::_defaultViewModelProperty =
    DependencyProperty::Register("DefaultViewModel",
    TypeName(PropertySet::typeid), TypeName(MainPage::typeid), nullptr);

/// <summary>
/// Gets an implementation of <see cref="PropertySet"/> designed to be
/// used as a trivial view model.
/// </summary>
PropertySet^ MainPage::DefaultViewModel::get()
{
    return safe_cast<PropertySet^>(GetValue(_defaultViewModelProperty));
}

DependencyProperty^ MainPage::_navigationHelperProperty =
    DependencyProperty::Register("NavigationHelper",
    TypeName(Common::NavigationHelper::typeid), TypeName(MainPage::typeid), nullptr);

/// <summary>
/// Gets an implementation of <see cref="NavigationHelper"/> designed to be
/// used as a trivial view model.
/// </summary>
Common::NavigationHelper^ MainPage::NavigationHelper::get()
{
    return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty));
}

/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached.  The Parameter
/// property provides the group to be displayed.</param>
void MainPage::OnNavigatedTo(NavigationEventArgs^ e)
{
    NavigationHelper->OnNavigatedTo(e);

    // Apply the animation that lightens the background.
    LightenBackground->Begin();

    LonLatController::Current->ResumeInput();

    // Check navigation parameter.
    // Might be a Forecast object from Search...
    auto forecast = dynamic_cast<Forecast^>(e->Parameter);

    // Might be JSON if activated from secondary tile. 
    if (forecast == nullptr && e->Parameter != nullptr && e->NavigationMode == NavigationMode::New)
    {
        auto jsonValue = Windows::Data::Json::JsonValue::Parse(e->Parameter->ToString());
        auto jsonObject = jsonValue->GetObject();
        auto command = jsonObject->GetNamedString("Command");
        if (command == "MoveToLocation")
        {
            auto uid = jsonObject->GetNamedString("Parameter");
            forecast = m_forecastManager->FindForecast(uid);
        }
    }

    // If the parameter is a Forecast object, then rotate the globe to that object and apply its pop-up animation.
    if (forecast != nullptr)
    {
        LonLatController::Current->AnimateToLatLonAsync(forecast->Latitude, forecast->Longitude).then([this, forecast]()
        {
            assert(IsMainThread());

            // Select it.
            forecast->ParentGroup->Current = forecast;

            // Apply the pop-up animation.
            ShowPopupAnimation(forecast);

        }, task_continuation_context::use_current());
    }

    // Register the rendering event, called every time XAML renders the screen.
    // The value may be already valid if activated from secondary tile while already running.
    if (m_eventToken.Value == 0)
    {
        m_eventToken = CompositionTarget::Rendering::add(ref new EventHandler<Object^>(this, &MainPage::OnRendering));
    }
}

/// <summary>
/// Invoked when this page will no longer be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached.  The Parameter
/// property provides the group to be displayed.</param>
void MainPage::OnNavigatedFrom(NavigationEventArgs^ e)
{
    NavigationHelper->OnNavigatedFrom(e);

    if (m_eventToken.Value != 0)
    {
        CompositionTarget::Rendering::remove(m_eventToken);
        m_eventToken.Value = 0;
    }

    // Ensure pin control is not active.
    if (m_pin->IsVisible)
    {
        CancelPin();
    }

    LonLatController::Current->SuspendInput();
}

/// <summary>
/// Populates the page with content passed during navigation.  Any saved state is also
/// provided when recreating a page from a prior session.
/// </summary>
/// <param name="navigationParameter">The parameter value passed to
/// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested.
/// </param>
/// <param name="pageState">A map of state preserved by this page during an earlier
/// session.  This will be null the first time a page is visited.</param>
void MainPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
{
    (void) sender;	// Unused parameter

    auto pageState = e->PageState;
    if (pageState == nullptr)
    {
        return;
    }

    // Load the LonLatController.
    // NOTE: Not sure this is the most appropraite place to do this, but it's convenient.
    if (pageState->HasKey("LonLatController.Settings"))
    {
        auto state = safe_cast<Map<String^, Object^>^>(pageState->Lookup("LonLatController.Settings"));
        LonLatController::Current->LoadInternalState(state);
    }

    // Load the locations that were in the popped-up state.
    if (pageState->HasKey("Popups"))
    {
        auto state = safe_cast<Map<String^, Object^>^>(pageState->Lookup("Popups"));
        for (auto pair : state)
        {
            auto forecast = m_forecastManager->FindForecast(pair->Key);
            if (forecast != nullptr)
            {
                ShowPopupAnimation(forecast);
            }
        }
    }
}

/// <summary>
/// Preserves state associated with this page in case the application is suspended or the
/// page is discarded from the navigation cache.  Values must conform to the serialization
/// requirements of <see cref="SuspensionManager::SessionState"/>.
/// </summary>
/// <param name="pageState">An empty map to be populated with serializable state.</param>
void MainPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
{
    (void) sender;	// Unused parameter

    auto pageState = e->PageState;

    // Save the LonLatController.
    // NOTE: Not sure this is the most appropraite place to do this, but it's convenient.
    if (pageState->HasKey("LonLatController.Settings"))
    {
        pageState->Remove("LonLatController.Settings");
    }
    auto state = ref new Map<String^, Object^>();
    LonLatController::Current->SaveInternalState(state);
    pageState->Insert("LonLatController.Settings", state);

    // Also cancel any animation so that 
    // 1) the user isn't surprised when the app is resumed and
    // 2) animation works correctly if the app is reactivated from the secondary tile.
    LonLatController::Current->CancelLatLonAnimation(true);

    // Remember which locations were in the popped-up state.
    if (pageState->HasKey("Popups"))
    {
        pageState->Remove("Popups");
    }
    state = ref new Map<String^, Object^>();
    for (const auto& pair : m_animationStoryboards)
    {
        if (pair.second.CurrentAnimation == pair.second.PopupAnimation)
        {
            state->Insert(pair.second.Forecast->UniqueIdentifier, PropertyValue::CreateBoolean(true));
        }
    }
    if (state->Size > 0)
    {
        pageState->Insert("Popups", state);
    }
}

// Updates the list of registered web services in the view model.
void MainPage::OnWebServiceRegistered(String^ serviceName)
{
    wchar_t buffer[1024] = { L'\0' };
    swprintf_s(buffer, L"Data provided by %s", serviceName->Data());
    auto message = ref new String(buffer);

    auto dataProviders = safe_cast<Vector<String^>^>(DefaultViewModel->Lookup("DataProviders"));
    // Add only if unique.
    if (find(begin(dataProviders), end(dataProviders), message) == end(dataProviders))
    {
        dataProviders->Append(message);
    }
}

// Retrieves the user's current location.
task<shared_ptr<Web::Location>> MainPage::GetCurrentLocationAsync()
{
    auto geolocator = ref new Windows::Devices::Geolocation::Geolocator();
    return create_task(geolocator->GetGeopositionAsync()).then([this, geolocator](task<Windows::Devices::Geolocation::Geoposition^> previousTask)
    {
        assert(IsMainThread());

        try
        {
            Windows::Devices::Geolocation::Geoposition^ geoPosition = previousTask.get();

            // Set result and return.
            const auto latitude = static_cast<float>(geoPosition->Coordinate->Point->Position.Latitude);
            const auto longitude = static_cast<float>(geoPosition->Coordinate->Point->Position.Longitude);
            auto civicAddress = geoPosition->CivicAddress;
            if (civicAddress->City != nullptr && civicAddress->State != nullptr && civicAddress->Country != nullptr)
            {
                auto result = make_shared<Web::Location>();
                result->Latlon.Latitude = latitude;
                result->Latlon.Longitude = longitude;
                result->AdminDistrict = civicAddress->City->Data();
                result->Locality = civicAddress->State->Data();
                result->CountryRegion = civicAddress->Country->Data();
                return task_from_result(result);
            }
            auto service = make_shared<Web::BingLocationService>();
            return service->GetLocationAsync(latitude, longitude);
        }
        catch (const Web::WebException&)
        {
            // The server returned an error response code.
        }
        catch (FailureException^)
        {
            // Not connected to the Internet.
        }
        catch (AccessDeniedException^)
        {
            // The user did not give permission for the app to access the user's location.
        }
        catch (Exception^)
        {
            assert(false); // TODO: Verify this
            // The XML response was poorly formatted.
        }
        // Cancel, because we couldn't get the location.
        cancel_current_task();
    });
}

// Displays the popup animation associated with the given Forecast.
void MainPage::ShowPopupAnimation(Forecast^ forecast)
{
    if (m_animationStoryboards.find(forecast->Uid) != end(m_animationStoryboards))
    {
        auto& val = m_animationStoryboards[forecast->Uid];
        auto popupAnimation = val.PopupAnimation;
        if (val.CurrentAnimation == popupAnimation)
        {
            // Animation is already current.
            return;
        }
        popupAnimation->Begin();
        val.CurrentAnimation = popupAnimation;
    }
    else
    {
        m_queuedPopupAnimations.emplace_back(forecast);
    }
}

// Displays the fade animation associated with the given Forecast.
void MainPage::ShowFadeAnimation(Forecast^ forecast)
{
    if (m_animationStoryboards.find(forecast->Uid) != end(m_animationStoryboards))
    {
        auto& val = m_animationStoryboards[forecast->Uid];
        auto hideAnimation = val.FadeAnimation;
        if (val.CurrentAnimation == hideAnimation)
        {
            // Animation is already current.
            return;
        }
        hideAnimation->Begin();
        val.CurrentAnimation = hideAnimation;
    }
    else
    {
        m_queuedFadeAnimations.emplace_back(forecast);
    }
}

// Displays the popup animation if the fade animation is current, and vice-versa.
void MainPage::ToggleAnimation(Forecast^ forecast)
{
    auto& val = m_animationStoryboards[forecast->Uid];
    auto popupAnimation = val.PopupAnimation;
    auto hideAnimation = val.FadeAnimation;
    auto currAnimation = val.CurrentAnimation;
    if (currAnimation == nullptr || currAnimation == hideAnimation)
    {
        currAnimation = popupAnimation;
    }
    else
    {
        assert(currAnimation == popupAnimation);
        currAnimation = hideAnimation;
    }
    currAnimation->Begin();
    val.CurrentAnimation = currAnimation;
}

// Animates the compass needle associated with the given Forecast.
void MainPage::ShowNeedleAnimation(Forecast^ forecast)
{
    auto it = m_animationStoryboards.find(forecast->Uid);
    if (it != end(m_animationStoryboards))
    {
        it->second.NeedleAnimation->Begin();
    }
}

// Updates the live tile.
task<void> MainPage::UpdateLiveTileAsync()
{
    m_updatingLiveTile = true;

    auto homeForecast = m_forecastManager->HomeForecast;
    auto currentForecast = m_forecastManager->CurrentLocation;

    // Collect all forecasts that 1) don't represent the Home or Current location and have forecast information 
    // for today and have a valid thumbnail image.
    vector<Forecast^> candidates;
    for (auto group : m_forecastManager->ForecastGroups)
    {
        for (auto forecast : group->Items)
        {
            if (forecast != homeForecast && forecast != currentForecast && forecast->Today != nullptr && forecast->ThumbnailImagePath != nullptr)
            {
                candidates.emplace_back(forecast);
            }
        }
    }

    // Shuffle the candidates and append the Home and Current forecasts, if they are valid. 
    random_shuffle(begin(candidates), end(candidates));

    if (homeForecast != nullptr && homeForecast->Today != nullptr)
    {
        candidates.push_back(homeForecast);
    }
    if (currentForecast != nullptr && currentForecast->Today != nullptr)
    {
        candidates.push_back(currentForecast);
    }

    // If there are no candidates, return a failed task.
    if (candidates.size() == 0)
    {
        Logger::LogWarning(this, "UpdateLiveTileAsync: No locations had valid weather info.");
        return create_task([]{});
    }

    // Make the offscreen Canvas elements visible. 
    HugeTile->Visibility = Windows::UI::Xaml::Visibility::Visible;
    WideTile->Visibility = Windows::UI::Xaml::Visibility::Visible;
    SquareTile->Visibility = Windows::UI::Xaml::Visibility::Visible;

    // Create a chain of tasks that complete one after another.
    auto tailTask = create_task([]{});

    // Communicate through the view model that this is for the primary tile.
    DefaultViewModel->Insert("IsSecondaryTileUpdate", false);

    // Queue up to five updates for display on the tile.
    for (uint32_t i = 0; i < 5; ++i)
    {
        if (candidates.size() == 0)
        {
            break;
        }
        auto forecast = candidates.back();
        candidates.pop_back();

        // Load the thumbnail image for the location from disk and put it on the square, wide, and large Canvas elements.
        auto thumbnailImage = ref new BitmapImage();
        tailTask = tailTask.then([this, forecast]()
        {
            assert(IsMainThread());
            ForecastTile = forecast; // this binds the Forecast data to the UI.
        }, task_continuation_context::use_current());

        tailTask = tailTask.then([forecast]()
        {
            assert(IsBackgroundThread());
            return StorageFile::GetFileFromPathAsync(forecast->ThumbnailImagePath);
        }, task_continuation_context::use_arbitrary()).then([](StorageFile^ file)
        {
            assert(IsBackgroundThread());
            return file->OpenAsync(FileAccessMode::Read);
        }, task_continuation_context::use_arbitrary()).then([this, thumbnailImage](IRandomAccessStream^ stream)
        {
            assert(IsMainThread());
            thumbnailImage->DecodePixelHeight = static_cast<int>(ForecastSquareTileImage->Height);
            thumbnailImage->DecodePixelWidth = static_cast<int>(ForecastSquareTileImage->Width);
            return thumbnailImage->SetSourceAsync(stream);
        }).then([this, thumbnailImage](task<void> previousTask)
        {
            assert(IsMainThread());
            try
            {
                previousTask.get();
                ForecastHugeTileImage->Source = thumbnailImage;
                ForecastWideTileImage->Source = thumbnailImage;
                ForecastSquareTileImage->Source = thumbnailImage;
            }
            catch (...)
            {
                cancel_current_task();
            }
        });

        // Load the weather image for the location from disk and put it on the square, wide, and large Canvas elements.
        auto weatherImage = ref new BitmapImage();
        tailTask = tailTask.then([forecast]()
        {
            assert(IsBackgroundThread());
            return StorageFile::GetFileFromApplicationUriAsync(ref new Uri(forecast->Today->WeatherImagePathUri));
        }, task_continuation_context::use_arbitrary()).then([](StorageFile^ file)
        {
            assert(IsBackgroundThread());
            return file->OpenAsync(FileAccessMode::Read);
        }, task_continuation_context::use_arbitrary()).then([this, weatherImage](IRandomAccessStream^ stream)
        {
            assert(IsMainThread());
            weatherImage->DecodePixelHeight = static_cast<int>(ForecastSquareTileWeatherImage->Height);
            weatherImage->DecodePixelWidth = static_cast<int>(ForecastSquareTileWeatherImage->Width);
            return weatherImage->SetSourceAsync(stream);
        }).then([this, weatherImage](task<void> previousTask)
        {
            assert(IsMainThread());
            try
            {
                previousTask.get();
                ForecastHugeTileWeatherImage->Source = weatherImage;
                ForecastWideTileWeatherImage->Source = weatherImage;
                ForecastSquareTileWeatherImage->Source = weatherImage;
            }
            catch (...)
            {
                cancel_current_task();
            }
        });

        // Now write the contents of the Canvas elements to disk as images.
        tailTask = tailTask.then([this](task<void> previousTask)
        {
            assert(IsMainThread());

            try
            {
                previousTask.get();
                return m_liveTileScheduler.UpdateLiveTileImagesAsync(ForecastTile, HugeTile, WideTile, SquareTile);
            }
            catch (const task_canceled&)
            {
                // Something went wrong in the chain. Just ignore this location.
                return create_task([]{});
            }

        }, task_continuation_context::use_current());
    }

    // When the task chain has completed, cleanup and update the live tile to point to the
    // tile images.
    return tailTask.then([this](task<void> previousTask)
    {
        assert(IsMainThread());

        // Hide the offscreen Canvas elements. 
        HugeTile->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        WideTile->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        SquareTile->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        ForecastTile = nullptr;

        m_liveTileScheduler.UpdateLiveTile();
        m_updatingLiveTile = false;

    }, task_continuation_context::use_current());
}

// Updates the secondary tile.
task<void> MainPage::UpdateSecondaryTileAsync(Forecast^ forecast)
{
    m_updatingSecondaryTile = true;

    // Communicate through the view model that this is for the secondary tile.
    DefaultViewModel->Insert("IsSecondaryTileUpdate", true);

    ForecastTile = forecast; // this binds the Forecast data to the UI.

    // Make the offscreen Canvas elements visible. 
    HugeTile->Visibility = Windows::UI::Xaml::Visibility::Visible;
    WideTile->Visibility = Windows::UI::Xaml::Visibility::Visible;
    SquareTile->Visibility = Windows::UI::Xaml::Visibility::Visible;

    // Load the thumbnail image for the location from disk and put it on the square, wide, and large Canvas elements.
    auto bitmapImage = ref new BitmapImage();
    return create_task(StorageFile::GetFileFromPathAsync(forecast->ThumbnailImagePath)).then([](StorageFile^ file)
    {
        assert(IsBackgroundThread());
        return file->OpenAsync(FileAccessMode::Read);
    }, task_continuation_context::use_arbitrary()).then([this, bitmapImage](IRandomAccessStream^ stream)
    {
        assert(IsMainThread());
        bitmapImage->DecodePixelHeight = static_cast<int>(ForecastSquareTileImage->Height);
        bitmapImage->DecodePixelWidth = static_cast<int>(ForecastSquareTileImage->Width);
        return bitmapImage->SetSourceAsync(stream);
    }).then([this, bitmapImage](task<void> previousTask)
    {
        assert(IsMainThread());
        try
        {
            previousTask.get();
            ForecastHugeTileImage->Source = bitmapImage;
            ForecastWideTileImage->Source = bitmapImage;
            ForecastSquareTileImage->Source = bitmapImage;
        }
        catch (Exception^ e)
        {
            cancel_current_task();
        }
        return m_liveTileScheduler.UpdateSecondaryTileImagesAsync(ForecastTile, HugeTile, WideTile, SquareTile);

    }).then([this](task<void> previousTask)
    {
        assert(IsMainThread());

        try
        {
            previousTask.get();
        }
        catch (const task_canceled&)
        {
            cancel_current_task();
        }

        // Hide the offscreen Canvas elements. 
        HugeTile->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        WideTile->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        SquareTile->Visibility = Windows::UI::Xaml::Visibility::Collapsed;

        auto forecast = ForecastTile;
        ForecastTile = nullptr;

        auto hugeLogoUri = m_liveTileScheduler.GetSecondaryTileHugeUri();
        auto wideLogoUri = m_liveTileScheduler.GetSecondaryTileWideUri();
        auto squareLogoUri = m_liveTileScheduler.GetSecondaryTileSquareUri();

        // This JSON text is held by the OS and passed to the app when the app is activated from the second tile.
        auto jsonObject = ref new Windows::Data::Json::JsonObject();
        jsonObject->Insert("Command", Windows::Data::Json::JsonValue::CreateStringValue("MoveToLocation"));
        jsonObject->Insert("Parameter", Windows::Data::Json::JsonValue::CreateStringValue(forecast->UniqueIdentifier));
        String^ tileActivationArguments = jsonObject->Stringify();

        // Create a 1x1 secondary tile.
        auto secondaryTile = ref new SecondaryTile(SecondaryTileId, forecast->DisplayName, tileActivationArguments, squareLogoUri, Windows::UI::StartScreen::TileSize::Wide310x150);
        secondaryTile->VisualElements->Square310x310Logo = hugeLogoUri;
        secondaryTile->VisualElements->Wide310x150Logo = wideLogoUri;

        // The tile is created and we can now attempt to pin the tile. 
        // Note that the status message is updated when the async operation to pin the tile completes. 
        return create_task(secondaryTile->RequestCreateForSelectionAsync(GetElementRect(GetButtonFromBar(BottomCommandBar, "PinToStartAppBarButton")), Windows::UI::Popups::Placement::Above));

    }, task_continuation_context::use_current()).then([this](task<bool> previousTask)
    {
        assert(IsMainThread());
        m_updatingSecondaryTile = false;

        try
        {
            previousTask.get();
        }
        catch (const task_canceled&)
        {
        }

    }, task_continuation_context::use_current());
}

// Updates pin UI.
void MainPage::ProcessPinLocation(shared_ptr<Web::Location> location)
{
    // We're touching the UI so ensure we run on the UI thread.
    run_async_non_interactive([this, location]()
    {
        assert(IsMainThread());

        m_pin->Location = location;
        m_pin->DisplayName = ref new String(location->Locality.c_str());
        m_pin->IsValid = m_pin->DisplayName->Length() > 0;
    });
}

// Processes pin locator errors.
void MainPage::ProcessPinError(Web::WebException error)
{
    Logger::LogWarning(this, "Error processing pin location.");
}

// Closes pin mode.
void MainPage::CancelPin()
{
    m_pin->IsVisible = false;
    TopAppBar->IsOpen = false;
    BottomAppBar->IsOpen = false;
    BottomAppBar->IsSticky = false;
    LonLatController::Current->ResumeInput();
}

// Called when the weather data changes.
void MainPage::OnWeatherDataChanged(Forecast^ forecast)
{
    // Weather data changed, so animate the compass needle to reflect the new wind direction.
    ShowNeedleAnimation(forecast);
}

// Called when the pointer is pressed. 
void MainPage::OnPointerPressed(CoreWindow ^sender, PointerEventArgs ^args)
{
    // If the pin control is active, set its Pressed property to true
    // if the pointer is in the control's bounds.
    if (m_pin->IsVisible)
    {
        auto pos = args->CurrentPoint->Position;
        if (pos.X > m_pin->X && pos.X < m_pin->X + m_pin->Width &&
            pos.Y > m_pin->Y && pos.Y < m_pin->Y + m_pin->Height)
        {
            m_pin->IsPressed = true;
        }
    }
}

// Called when the pointer is moved.
void MainPage::OnPointerMoved(CoreWindow^ sender, PointerEventArgs^ e)
{
    // Update the location of the pin control if the pointer 
    // is pressed and the pin control's Pressed property is set.
    auto currentPoint = e->CurrentPoint;

    if (currentPoint->IsInContact && m_pin->IsPressed)
    {
        if (m_lastPointValid)
        {
            Point delta(
                currentPoint->Position.X - m_lastPoint.X,
                currentPoint->Position.Y - m_lastPoint.Y
                );
            m_pin->X += delta.X;
            m_pin->Y += delta.Y;
            m_pin->IsDirty = true;
        }
        m_lastPoint = currentPoint->Position;
        m_lastPointValid = true;
    }
    else
    {
        m_lastPointValid = false;
    }
}

// Called when the pointer is released.
void MainPage::OnPointerReleased(CoreWindow^ window, PointerEventArgs^ e)
{
    (void) window;
    (void) e;

    // Invalidate the pin.
    m_lastPointValid = false;
    m_pin->IsPressed = false;
}

// Called when the pin control's size changes.
void MainPage::OnPinSizeChanged(Object^ sender, SizeChangedEventArgs^ e)
{
    // TODO: Can (should) we use two-way binding instead?
    Button^ button = safe_cast<Button^>(sender);
    m_pin->Width = button->ActualWidth;
    m_pin->Height = button->ActualHeight;
}

// Called when the user chooses the Save command when pin mode is active.
void MainPage::SavePinAppBarButton_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Add the location to the global collection.
    auto forecast = m_forecastManager->AddForecast(m_pin->Location->Latlon,
        ref new String(m_pin->Location->Locality.c_str()),
        ref new String(m_pin->Location->AdminDistrict.c_str()),
        ref new String(m_pin->Location->CountryRegion.c_str()));

    // Select it.
    forecast->ParentGroup->Current = forecast;

    // Apply the pop-up animation.
    ShowPopupAnimation(forecast);

    // Close app bars.
    m_pin->IsVisible = false;
    TopAppBar->IsOpen = false;
    BottomAppBar->IsOpen = false;
    BottomAppBar->IsSticky = true;
    LonLatController::Current->ResumeInput();

    // Save locations file to disk in the background.
    LocationData::Current().SaveUserLocationsAsync().then(ObserveException<void>(m_exceptionPolicy));
}

// Called when the user chooses the Cancel command when pin mode is active.
void MainPage::CancelPinAppBarButton_Click(Object^ sender, RoutedEventArgs^ e)
{
    CancelPin();
}

// Called every time XAML decides to render a frame.
void MainPage::OnRendering(Object^ sender, Object^ args)
{
    (void) sender;
    (void) args;

    if (m_updatingLiveTile || m_updatingSecondaryTile)
    {
        return;
    }

    m_tileUpdateTimer->Update();
    if (m_tileUpdateTimer->Total >= TileUpdateInterval)
    {
        TileUpdateInterval = TimeUpdateIntervalFuture; // Ensure future updates are made at the longer interval.

        auto evt = Logger::LogAsyncAction(this, "Updating live tile...");

        UpdateLiveTileAsync().then([this, evt](task<void> previousTask)
        {
            assert(IsMainThread());
            m_tileUpdateTimer->Reset();
            try
            {
                previousTask.get();
                evt.set(true);
            }
            catch (...)
            {
                evt.set(false);
            }
            return previousTask;

        }, task_continuation_context::use_current()).then(ObserveException<void>(m_exceptionPolicy));
    }
}

// Called when the user clicks the 'next' button on the UI to rotate 
// through locations that are in close proximity with one another.
void MainPage::GroupNextButton_Click(Object^ sender, RoutedEventArgs^ e)
{
    auto group = safe_cast<ForecastGroup^>((safe_cast<AppBarButton^>(sender))->Tag);
    auto current = group->Current;
    if (current != nullptr)
    {
        // Apply the fade animation.
        ShowFadeAnimation(current);
    }
    unsigned int size = group->Items->Size;
    for (unsigned int i = 0; i < size; ++i)
    {
        auto forecast = group->Items->GetAt(i);
        if (forecast == current)
        {
            i++;
            if (i == size)
            {
                i = 0;
            }
            forecast = group->Items->GetAt(i);
            // Apply the pop-up animation.
            ShowPopupAnimation(forecast);

            group->Current = forecast;
            break;
        }
    }
}

// Called when the user chooses the Pin to Start command.
void MainPage::PinToStart_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Update the data elements.
    DefaultViewModel->Insert("VisibleForecastsSortedAlphabetically", m_forecastManager->VisibleForecastsSortedAlphabetically);

    PinToStartPopup->IsOpen = true;
}

// Called whenever the number of items in the Pin to Start popup changes.
void MainPage::PinToStartSizeChanged(Object^ sender, SizeChangedEventArgs^ e)
{
    // Ensure the popup rests on top of the command bar.
    PopupPlacementHelper(PinToStartPopup, GetButtonFromBar(BottomCommandBar, "PinToStartAppBarButton"), e->NewSize.Height);
}

// Called when the user chooses an item from the Pin to Start popup. 
void MainPage::PinToStartItem_Click(Object^ sender, RoutedEventArgs^ e)
{
    PinToStartPopup->IsOpen = false;
    TopAppBar->IsOpen = false;
    BottomAppBar->IsOpen = false;

    auto forecast = safe_cast<Forecast^>((safe_cast<ButtonBase^>(sender))->DataContext);

    auto evt = Logger::LogAsyncAction(this, "Updating secondary tile...");

    // Set up the secondary tile.
    UpdateSecondaryTileAsync(forecast).then([evt](task<void> previousTask)
    {
        try
        {
            previousTask.get();
            evt.set(true);
        }
        catch (...)
        {
            evt.set(false);
        }
        return previousTask;

    }).then(ObserveException<void>(m_exceptionPolicy));
}

// Called when the user chooses the Home command.
// Takes the user to their home location.
void MainPage::Home_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Ensure no other animation is active.
    LonLatController::Current->CancelLatLonAnimation(false);

    auto forecast = m_forecastManager->HomeForecast;
    if (forecast != nullptr)
    {
        TopAppBar->IsOpen = false;
        BottomAppBar->IsOpen = false;

        // Begin animation.
        LonLatController::Current->AnimateToLatLonAsync(forecast->Latitude, forecast->Longitude).then([this, forecast]()
        {
            assert(IsMainThread());

            // After animation completes...

            // Select it.
            forecast->ParentGroup->Current = forecast;

            // Apply the pop-up animation.
            ShowPopupAnimation(forecast);

        }, task_continuation_context::use_current());
    }
}

// Called when the user chooses the Current command.
// Takes the user to their current location.
void MainPage::Current_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Ensure no other animation is active.
    LonLatController::Current->CancelLatLonAnimation(false);

    shared_ptr<Forecast^> forecast = make_shared<Forecast^>(nullptr);

    GetCurrentLocationAsync().then([this, forecast](shared_ptr<Web::Location> result)
    {
        assert(IsMainThread());

        *forecast = m_forecastManager->AddForecast(result->Latlon,
            ref new String(result->Locality.c_str()),
            ref new String(result->AdminDistrict.c_str()),
            ref new String(result->CountryRegion.c_str()));

        TopAppBar->IsOpen = false;
        BottomAppBar->IsOpen = false;

        // Begin animation.
        return LonLatController::Current->AnimateToLatLonAsync((*forecast)->Latitude, (*forecast)->Longitude);
    }).then([this, forecast]()
    {
        assert(IsMainThread());

        // After animation completes...

        // Select it.
        (*forecast)->ParentGroup->Current = *forecast;

        // Apply the pop-up animation.
        ShowPopupAnimation(*forecast);

    }).then([](task<void> previousTask)
    {
        try
        {
            previousTask.get();
        }
        catch (const task_canceled&)
        {
            // GetCurrentLocationAsync cancels if it can't get the location.
            // Show a message box.
            String^ message = "We weren't able to retrieve your current location";
            String^ title = "Sorry";
            MessageDialog::ShowAcceptDialogAsync(message, title);
        }

    }).then(ObserveException<void>(m_exceptionPolicy));
}

// Called when the user chooses the Random command.
// Takes the user to a random location.
void MainPage::Random_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Create a reusable random number generator.
    static std::mt19937 rand(static_cast<unsigned int>(chrono::system_clock::now().time_since_epoch().count()));

    // Ensure no other animation is active.
    LonLatController::Current->CancelLatLonAnimation(false);

    DefaultViewModel->Insert("IsAsyncOperationActive", true);
    shared_ptr<Forecast^> randomForecast = make_shared<Forecast^>(nullptr);

    create_task([this]()
    {
        // Throw darts at the world until we hit a city.
        for (;;)
        {
            array<float32, 2> plusminus = { -1, +1 };
            float32 lat = plusminus[rand() % 2] * (rand() % 90 + (rand() % 1000 / 1000.0f));
            float32 lon = plusminus[rand() % 2] * (rand() % 180 + (rand() % 1000 / 1000.0f));

            // OK to block because we're on a background thread.
            auto location = m_locationService->GetLocationAsync(lat, lon).get();
            if (!location->Locality.empty())
            {
                return location;
            }
        }
    }).then([this, randomForecast](shared_ptr<Web::Location> location)
    {
        assert(IsMainThread());

        DefaultViewModel->Insert("IsAsyncOperationActive", false);

        // Add the forecast to the collection.
        auto forecast = m_forecastManager->AddForecast(location->Latlon,
            ref new String(location->Locality.c_str()),
            ref new String(location->AdminDistrict.c_str()),
            ref new String(location->CountryRegion.c_str()));

        // Select it.
        forecast->ParentGroup->Current = forecast;

        *randomForecast = forecast;

        return LonLatController::Current->AnimateToLatLonAsync(forecast->Latitude, forecast->Longitude);

    }, task_continuation_context::use_current()).then([this, randomForecast]()
    {
        assert(IsMainThread());

        auto forecast = *randomForecast;

        // Apply the pop-up animation.
        ShowPopupAnimation(forecast);

        m_pin->IsVisible = false;
        TopAppBar->IsOpen = false;
        BottomAppBar->IsOpen = false;

        // Save locations file to disk in the background.
        return LocationData::Current().SaveUserLocationsAsync();

    }, task_continuation_context::use_current()).then(ObserveException<void>(m_exceptionPolicy));
}

// Called when the user chooses the Drop Pin command.
// Enables pin mode.
void MainPage::DropPin_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Keep app bar up.
    BottomAppBar->IsSticky = true;

    // Activate the pin control.
    m_pin->IsDirty = true;
    m_pin->IsVisible = true;

    // Move pin to middle of screen.
    auto window = CoreWindow::GetForCurrentThread();
    m_pin->X = window->Bounds.Width / 2 - m_pin->Width / 2;
    m_pin->Y = window->Bounds.Height / 2 - m_pin->Height;

    // Suspend the input controller.
    LonLatController::Current->SuspendInput();
}

// Called when the user chooses the Change Home command.
// Brings up a popup that lets the user choose a different home location.
void MainPage::ChangeHome_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Update the data elements.
    DefaultViewModel->Insert("AllForecastsSortedAlphabetically", m_forecastManager->AllForecastsSortedAlphabetically);

    ChangeHomePopup->IsOpen = true;
}

// Called whenever the number of items in the Change Home popup changes.
void MainPage::ChangeHome_SizeChanged(Object^ sender, SizeChangedEventArgs^ e)
{
    // Ensure the popup rests on top of the command bar.
    PopupPlacementHelper(ChangeHomePopup, GetButtonFromBar(BottomCommandBar, "ChangeHomeAppBarButton"), e->NewSize.Height);
}

// Called when the user chooses an item from the Change Home popup. 
void MainPage::ChangeHomeItem_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Close the popup and app bars.
    ChangeHomePopup->IsOpen = false;
    TopAppBar->IsOpen = false;
    BottomAppBar->IsOpen = false;

    // Set new home location.
    auto forecast = safe_cast<Forecast^>((safe_cast<ButtonBase^>(sender))->DataContext);
    m_forecastManager->HomeForecast = forecast;
}

// Called when the user taps a location on the UI.
void MainPage::LocationButton_Click(Object^ sender, RoutedEventArgs^ e)
{
    auto button = safe_cast<Button^>(sender);
    auto forecast = safe_cast<Forecast^>(button->DataContext);
    // Toggle the animation.
    ToggleAnimation(forecast);
}

// Called when the 'WeatherInfo' Canvas element for each location is loaded.
void MainPage::WeatherInfo_Loaded(Object^ sender, RoutedEventArgs^ e)
{
    //
    // Associate the animations in the Canvas with the Forecast object.
    //

    auto canvas = safe_cast<Canvas^>(sender);
    auto show = find_if(begin(canvas->Resources), end(canvas->Resources), [](IKeyValuePair<Object^, Object^>^ kvp)
    {
        return kvp->Key->ToString() == "ShowWeatherInfoStoryboard";
    });
    auto hide = find_if(begin(canvas->Resources), end(canvas->Resources), [](IKeyValuePair<Object^, Object^>^ kvp)
    {
        return kvp->Key->ToString() == "HideWeatherInfoStoryboard";
    });
    auto needle = find_if(begin(canvas->Resources), end(canvas->Resources), [](IKeyValuePair<Object^, Object^>^ kvp)
    {
        return kvp->Key->ToString() == "CompassNeedleAnimation";
    });
    auto forecast = safe_cast<Forecast^>(canvas->DataContext);

    ForecastAnimations animations =
    {
        forecast,
        safe_cast<Storyboard^>((*show)->Value),
        safe_cast<Storyboard^>((*hide)->Value),
        nullptr,
        static_cast<Storyboard^>((*needle)->Value)
    };
    m_animationStoryboards.insert(make_pair(forecast->Uid, animations));

    // Apply the animation if it was triggered to run before the XAML control was created.
    auto it = find(begin(m_queuedPopupAnimations), end(m_queuedPopupAnimations), forecast);
    if (it != end(m_queuedPopupAnimations))
    {
        ShowPopupAnimation(forecast);
        m_queuedPopupAnimations.erase(it);
    }

    it = find(begin(m_queuedFadeAnimations), end(m_queuedFadeAnimations), forecast);
    if (it != end(m_queuedFadeAnimations))
    {
        ShowFadeAnimation(forecast);
        m_queuedFadeAnimations.erase(it);
    }
}

// Called when the user chooses a different date from the top app bar.
void MainPage::DateStripGridView_ItemClick(Object^ sender, ItemClickEventArgs^ e)
{
    DateStripGridView->SelectedItem = e->ClickedItem;
    auto date = safe_cast<DateTime>(DateStripGridView->SelectedItem);
    // Update dates.
    m_forecastManager->PreferredDate = date;
    m_forecastManager->CurrentDate = date;
}

// Called when the top app bar is opened.
void MainPage::TopAppBar_Opened(Object^ sender, Object^ e)
{
    // Give the date strip an initial selection if it doesn't already have one.
    if (DateStripGridView->SelectedIndex == -1 && DateStripGridView->Items->Size > 0)
    {
        DateStripGridView->SelectedIndex = 0;
    }

    // Resume the input controller.
    LonLatController::Current->SuspendInput();
}

// Called when the top app bar is closed.
void MainPage::TopAppBar_Closed(Object^ sender, Object^ e)
{
    // Resume the input controller.
    LonLatController::Current->ResumeInput();
}

// Called when the user chooses the Manage Locations command.
void MainPage::Locations_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Navigate to the Manage Locations page.
    Frame->Navigate(TypeName(LocationsPage::typeid), nullptr);
}

// Called when the user chooses the Refresh command.
void MainPage::Refresh_Click(Object^ sender, RoutedEventArgs^ e)
{
    const utility::datetime def;
    // Reset the last checkin time. Each Forecast object will refresh its data as needed.
    for (auto forecastGroup : m_forecastManager->ForecastGroups)
    {
        for (auto forecast : forecastGroup->Items)
        {
            forecast->LastCheckin = def;
        }
    }
}
